import { trim, getAngle, getDistance } from '../index'

export declare type LiquidStyle = string | CanvasGradient | CanvasPattern

export declare type LiquidColor = LiquidStyle | { offset: number, color: string }[] | ((ctx: CanvasRenderingContext2D) => LiquidStyle)

export declare interface LiquidOptions {
  color: LiquidColor // 激活颜色

  waveCount: number // 波纹数量
  waveColor: LiquidColor // 波纹颜色

  gap: number // 线条间隙
  gapColor: LiquidColor // 线条间隙颜色

  strokeWidth: number // 线条宽度
  strokeInActiveColor: LiquidColor // 线条未激活部分颜色
  strokeActiveColor: LiquidColor // 线条激活部分颜色
  strokeLineCap: 'butt' | 'round' | 'square' // 线条两端样式
}

export default class Liquid {
  constructor(el: HTMLElement, options: Partial<LiquidOptions>)
  constructor(options: Partial<LiquidOptions> & { el: HTMLElement })
  constructor(el: any, options?: Partial<LiquidOptions>) {
    this.init(el, options)
  }

  // 设备像素比
  private readonly dpr: number = window.devicePixelRatio || 1

  // 水球最小值
  private readonly minValue: number = 0

  // 水球最大值
  private readonly maxValue: number = 100

  // 动画过程中保留的最大小数位数
  private readonly maxDigits: number = 4

  private readonly defaultOptions: Partial<LiquidOptions> = {
    color: '#3693FF',
    waveCount: 2,
    gap: 0,
    gapColor: '#FFFFFF',
    strokeWidth: 16,
    strokeInActiveColor: '#F9FBFF',
    strokeLineCap: 'round',
  }

  // 水球配置
  private options: Partial<LiquidOptions>

  private color: LiquidOptions['color'] = '#3693FF'

  private waveColor: LiquidOptions['waveColor'] = this.color

  private waveCount: LiquidOptions['waveCount'] = 2

  private gap: LiquidOptions['gap'] = 0

  private gapColor: LiquidOptions['gapColor'] = '#FFFFFF'

  private strokeWidth: LiquidOptions['strokeWidth'] = 16

  private strokeInActiveColor: LiquidOptions['strokeInActiveColor'] = '#F9FBFF'

  private strokeActiveColor: LiquidOptions['strokeActiveColor'] = this.color

  private strokeLineCap: LiquidOptions['strokeLineCap'] = 'round'

  // canvas
  private canvas: HTMLCanvasElement

  // canvas 上下文
  private ctx: CanvasRenderingContext2D

  // 离屏 canvas 元素
  private offscreenCanvas: HTMLCanvasElement

  // 离屏 canvas 上下文
  private offscreenCtx: CanvasRenderingContext2D

  // requestAnimationFrame id
  private raf: number = 0

  // 水波纹相位
  private φ: number = 0

  // 水球当前值
  private currentValue: number = 0

  // 水球目标值
  private targetValue: number = 0

  // 监视器
  private resizeObserver: ResizeObserver

  // 圆点坐标
  private get center() {
    return [this.canvas.width / 2, this.canvas.height / 2]
  }

  // 外圆半径
  private get outerRadius() {
    const size = Math.min(this.canvas.width, this.canvas.height)

    return size / 2 - this.pixelToDevice(this.strokeWidth!) / 2
  }

  // 内圆半径
  private get innerRadius() {
    const size = Math.min(this.canvas.width, this.canvas.height)

    const strokeWidth = this.pixelToDevice(this.strokeWidth)

    const gap = this.pixelToDevice(this.gap)

    return size / 2 - strokeWidth - (gap / 2)
  }

  // 整数部分字体大小
  private get integerFontSize() {
    return Math.floor(this.outerRadius / 2.5)
  }

  // 小数部分字体大小
  private get decimalFontSize() {
    return Math.floor(this.integerFontSize / 1.75)
  }

  // 目标值小数位数
  private get digits() {
    const str = this.targetValue.toString()
    const match = str.match(/\.(\d+)/)

    if (match) return match[1].length

    return 0
  }

  // 当前值整数部分
  private get currentInteger() {
    return `${Math.floor(this.currentValue)}`
  }

  // 当前值小数部分
  private get currentDecimal() {
    const decimal = this.currentValue.toFixed(this.digits).split('.')[1]

    return decimal ? `.${decimal}` : ''
  }

  // 动画方向 (> 0 向下; === 0 不变; < 0 向上)
  private get isDown() {
    return this.currentValue > this.targetValue
  }

  // 动画步长
  private get step() {
    const digits = Math.min(this.digits, this.maxDigits)

    const power = (n: number): number => {
      return n ? Math.pow(10, -n) + power(n - 1) : 1
    }

    return power(digits) * (this.isDown ? -1 : 1)
  }

  // 初始化
  private init(el: HTMLElement, options: Partial<LiquidOptions>): void
  private init(options: Partial<LiquidOptions> & { el: HTMLElement }): void
  private init(el: any, options?: Partial<LiquidOptions>) {
    if (!el) throw new Error(`[Error] argument[0] must be an HTMLElement or Object with el attribute that is an HTMLElement but got ${el}`)

    if (el instanceof HTMLElement === false) {
      if (!el.el) throw new Error(`[Error] argument[0] must be an HTMLElement or Object with el attribute that is an HTMLElement but got ${el.el}`)

      options = el

      el = el.el
    }

    this.options = { ...this.defaultOptions, ...options }

    this.gap = this.options.gap
    this.waveCount = Math.min(Math.max(this.options.waveCount, 0), 3)
    this.strokeWidth = this.options.strokeWidth
    this.strokeLineCap = this.options.strokeLineCap

    if (el.tagName.toLowerCase() === 'canvas') {
      this.canvas = el as HTMLCanvasElement
    } else {
      this.canvas = document.createElement('canvas')

      el.innerHTML = ''

      el.appendChild(this.canvas)
    }

    this.canvas.innerText = '当前浏览器不支持canvas，请升级或更换浏览器！'

    this.offscreenCanvas = document.createElement('canvas')

    this.resizeObserver = new ResizeObserver(() => {
      this.resize()
    })

    this.resizeObserver.observe(this.canvas)
  }

  // 将逻辑像素转换为物理像素
  private pixelToDevice(pixel: number) {
    return Math.floor(pixel * this.dpr)
  }

  /**
   * 创建 canvas 线性渐变
   * @param {color} color – linear-gradient([<angle> || to <side-or-corner>][, <color-stop> [, <color-stop>]+])
   * @param {angle} – deg | rad | grad | turn left
   * @param {side-or-corner} - top | right | bottom | left | top left | top right | bottom right | bottom left
   * @param {color-stop} - <color> [ <percentage> | <length> ]? [<percentage> | <length>]?
   * @returns {gradient} - CanvasGradient
   */
  private createLinearGradient(color: string) {
    if (!color) return

    const regex = /linear-gradient\((.*)\)/g

    const match = regex.exec(color)

    if (!match) throw new Error(`[Error] argument ${color} is invalid linear-gradient`)

    // 获取渐变函数内所有参数
    const args = match[1].split(/,(?![^(]*\))/).map(s => trim(s))

    if (!args.length) throw new Error(`[Error] argument ${color} is invalid linear-gradient`)

    const directionMap = {
      'to top left': '-45deg',
      'to top': '0deg',
      'to top right': '45deg',
      'to right top': '45deg',
      'to right': '90deg',
      'to right bottom': '135deg',
      'to bottom right': '135deg',
      'to bottom': '180deg',
      'to bottom left': '-135deg',
      'to left bottom': '-135deg',
      'to left': '-90deg',
      'to left top': '-45deg',
    }

    // 是否有 <angle> | <side-or-corner>
    const hasAngleOrSideCorner = directionMap[args[0] as keyof typeof directionMap] ||
      args[0].endsWith('deg') ||
      args[0].endsWith('rad') ||
      args[0].endsWith('grad') ||
      args[0].endsWith('turn')

    const initialAngle = hasAngleOrSideCorner ?
      directionMap[args[0] as keyof typeof directionMap] || args[0] : '0deg'

    // 由于 css 是从 -0.5π 开始，坐标周却是从 0π 开始，所以加上 0.5π 
    const angle = getAngle(initialAngle, 'rad') - getAngle('90deg', 'rad')

    const colorStops = hasAngleOrSideCorner ? args.slice(1) : args

    // canvas 中心坐标至 canvas 四角的距离
    const size = Math.hypot(this.canvas.width, this.canvas.height) / 2

    // 起点坐标
    const x1 = this.center[0] - size * Math.cos(angle)
    // 由于坐标轴向下，故乘以 -1
    const y1 = this.center[1] + size * Math.sin(angle) * -1

    // 结束坐标
    const x2 = this.center[0] + size * Math.cos(angle)
    const y2 = this.center[1] - size * Math.sin(angle) * -1

    const gradient = this.ctx.createLinearGradient(
      Math.max(0, Math.min(x1, this.canvas.width)),
      Math.max(0, Math.min(y1, this.canvas.height)),
      Math.max(0, Math.min(x2, this.canvas.width)),
      Math.max(0, Math.min(y2, this.canvas.height)),
    )

    const getOffset = (offset: string, index: number) => {
      if (!offset) {
        if (index === 0) return 0
        if (index === colorStops.length - 1) return 1

        return index / colorStops.length
      }

      if (offset.endsWith('%')) return parseFloat(offset) / 100

      if (offset.endsWith('px')) {
        const distance = getDistance(x1, y1, x2, y2)

        return this.pixelToDevice(parseFloat(offset)) / distance
      }

      throw new Error(`argument ${offset} is not supported!`)
    }

    colorStops.forEach((item, index) => {
      const list = item.split(' ')
      const len = list.length

      if (!len) throw new Error(`[Error] argument ${item} is invalid`)

      const colorStop = list[0]

      if (!index) gradient.addColorStop(0, colorStop)

      let startOffset = getOffset(list[1], index)
      let endOffset = getOffset(list[1], index)

      if (list[2]) endOffset = getOffset(list[2], index)

      gradient.addColorStop(startOffset, colorStop)
      gradient.addColorStop(endOffset, colorStop)

      if (index === colorStops.length - 1) gradient.addColorStop(1, colorStop)
    })

    return gradient
  }

  /**
   * 创建 canvas 径向渐变
   * @param {color} color – radial-gradient([<ending-shape> || <size> at <position>] [, <color-stop> [, <color-stop>]+)
   * @param {ending-shape} – circle | ellipse
   * @param {size} - closest-side | closest-corner | farthest-side | farthest-corner | [ <length-percentage> | <length> ]{2}
   * @param {position} - [ left | center | right | top | bottom | <length-percentage> ]  |
   *                     [ left | center | right ] && [ top | center | bottom ]  |
   *                     [ left | center | right | <length-percentage> ] [ top | center | bottom | <length-percentage> ]  |
   *                     [ [ left | right ] <length-percentage> ] && [ [ top | bottom ] <length-percentage> ]
   * @param {color-stop} - <color> [ <percentage> | <length> ]? [<percentage> | <length>]?
   * @returns {gradient} - CanvasGradient
   */
  private createRadialGradient(color: string) {
    if (!color) return

    const regex = /radial-gradient\((.*)\)/g

    const match = regex.exec(color)

    if (!match) throw new Error(`[Error] argument ${color} is invalid radial-gradient`)

    const args = match[1].split(/,(?![^(]*\))/).map(s => trim(s))

    if (!args.length) throw new Error(`[Error] argument ${color} is invalid radial-gradient`)

    // 渐变形状
    const hasEndingShape = args[0].includes('circle') || args[0].includes('ellipse')

    // 开始坐标
    const positions = args[0].includes('at') ?
      args[0].split('at ')[1].trim().split(' ') : null

    // 渐变大小
    const hasSize = args[0].includes('closest-side') ||
      args[0].includes('closest-corner') ||
      args[0].includes('farthest-side') ||
      args[0].includes('farthest-corner') ||
      /^[1-9]/.test(args[0].split(' ')[0])

    // 获取x坐标
    const getVerticalPosition = (x: string) => {
      x = x || 'center'

      switch (x) {
        case 'left': return 0
        case 'center': return this.center[0]
        case 'right': return this.canvas.width
        case 'top': return this.center[0]
        case 'bottom': return this.center[0]
      }

      if (x.endsWith('%')) return this.canvas.width * parseFloat(x) / 100

      if (x.endsWith('px')) return this.pixelToDevice(parseFloat(x))

      throw new Error(`[Error] only css units '%' and 'px' are supported!`)
    }

    // 获取y坐标
    const getHorizontalPosition = (y: string) => {
      y = y || 'center'

      switch (y) {
        case 'top': return 0
        case 'center': return this.center[1]
        case 'bottom': return this.canvas.height
        case 'left': return this.center[1]
        case 'right': return this.center[1]
      }

      if (y.endsWith('%')) return this.canvas.height * parseFloat(y) / 100

      if (y.endsWith('px')) return this.pixelToDevice(parseFloat(y))

      throw new Error(`[Error] only css units '%' and 'px' are supported!`)
    }

    // 开始/结束圆形坐标
    const x0 = getVerticalPosition(positions && positions[0])
    const y0 = getHorizontalPosition(positions && (positions[1] || positions[0]))

    const gradient = this.ctx.createRadialGradient(x0, y0, 0, x0, y0, this.outerRadius);

    const colorStops = positions || hasEndingShape || hasSize ? args.slice(1) : args

    const getOffset = (offset: string, index: number) => {
      if (!offset) {
        if (index === 0) return 0

        if (index === colorStops.length - 1) return 1

        return index / colorStops.length
      }

      if (offset.endsWith('%')) return parseFloat(offset) / 100

      if (offset.endsWith('px')) {
        const distance = getDistance(x0, y0, this.center[0], this.center[0])
        const size = distance + this.outerRadius

        return this.pixelToDevice(parseFloat(offset)) / size
      }

      throw new Error(`argument ${offset} is not supported!`)
    }

    colorStops.forEach((item, index) => {
      const list = item.split(' ')
      const len = list.length

      if (!len) throw new Error(`[Error] colorStop ${item} is invalid`)

      const colorStop = list[0]

      if (!index) gradient.addColorStop(0, colorStop)

      let startOffset = getOffset(list[1], index)
      let endOffset = getOffset(list[1], index)

      if (list[2]) endOffset = getOffset(list[2], index)

      gradient.addColorStop(startOffset, colorStop)
      gradient.addColorStop(endOffset, colorStop)

      if (index === colorStops.length - 1) gradient.addColorStop(1, colorStop)
    })

    return gradient
  }

  /**
   * 创建 canvas 锥形渐变
   * @param {color} color – conic-gradient([from <angle> || at <position> [, <color-stop> [, <color-stop>]+])
   * @param {angle} – deg | rad | grad | turn
   * @param {position} - [ left | center | right | top | bottom | <length-percentage> ]  |
   *                     [ left | center | right ] && [ top | center | bottom ]  |
   *                     [ left | center | right | <length-percentage> ] [ top | center | bottom | <length-percentage> ]  |
   *                     [ [ left | right ] <length-percentage> ] && [ [ top | bottom ] <length-percentage> ]
   * @param {color-stop} - <color> [ <angle> ]? [<angle>]?
   * @returns {gradient} - CanvasGradient
   */
  private createConicGradient(color: string) {
    if (!color) return

    const regex = /conic-gradient\((.*)\)/g

    const match = regex.exec(color)

    if (!match) throw new Error(`[Error] argument ${color} is not conic-gradient`)

    const args = match[1].split(/,(?![^(]*\))/).map(s => trim(s))

    if (!args.length) throw new Error(`[Error] argument ${color} is invalid conic-gradient`)

    // 起始角度
    const fromAngle = args[0].includes('from') ? args[0].split('from ')[1].trim() : null

    // 开始坐标
    const positions = args[0].includes('at') ? args[0].split('at ')[1].trim().split(' ') : null

    // 获取x坐标
    const getVerticalPosition = (x: string) => {
      x = x || 'center'

      switch (x) {
        case 'left': return 0
        case 'center': return this.center[0]
        case 'right': return this.canvas.width
        case 'top': return this.center[0]
        case 'bottom': return this.center[0]
      }

      if (x.endsWith('%')) return this.canvas.width * parseFloat(x) / 100

      if (x.endsWith('px')) return this.pixelToDevice(parseFloat(x))

      throw new Error('[Error] only css units "%" and "px" are supported!')
    }

    // 获取y坐标
    const getHorizontalPosition = (y: string) => {
      y = y || 'center'

      switch (y) {
        case 'top': return 0
        case 'center': return this.center[1]
        case 'bottom': return this.canvas.height
        case 'left': return this.center[1]
        case 'right': return this.center[1]
      }

      if (y.endsWith('%')) return this.canvas.height * parseFloat(y) / 100

      if (y.endsWith('px')) return this.pixelToDevice(parseFloat(y))

      throw new Error('[Error] only css units "%" and "px" are supported!')
    }

    // 开始/结束圆形坐标
    const x0 = getVerticalPosition(positions && positions[0])
    const y0 = getHorizontalPosition(positions && (positions[1] || positions[0]))

    const angle = getAngle(fromAngle || '0deg', 'rad') - getAngle('90deg', 'rad')

    const gradient = this.ctx.createConicGradient(angle, x0, y0);

    const colorStops = fromAngle || positions ? args.slice(1) : args

    const getOffset = (offset: string, index: number) => {
      if (!offset) {
        if (index === 0) return 0

        if (index === colorStops.length - 1) return 1

        return index / colorStops.length
      }

      const currentAngle = getAngle(offset, 'rad') - getAngle('90deg', 'rad')

      return (currentAngle - angle) / (2 * Math.PI)
    }

    colorStops.forEach((item, index) => {
      const list = item.split(' ')
      const len = list.length

      if (!len) throw new Error(`[Error] colorStop ${item} is invalid`)

      const colorStop = list[0]

      if (!index) gradient.addColorStop(0, colorStop)

      let startOffset = getOffset(list[1], index)
      let endOffset = getOffset(list[1], index)

      if (list[2]) endOffset = getOffset(list[2], index)

      gradient.addColorStop(startOffset, colorStop)
      gradient.addColorStop(endOffset, colorStop)

      if (index === colorStops.length - 1) gradient.addColorStop(1, colorStop)
    });

    return gradient
  }

  // 创建颜色
  private createLiquidColor(color: LiquidColor) {
    if (!color) return

    if (typeof color === 'function') return color(this.ctx)

    if (color instanceof CanvasGradient || color instanceof CanvasPattern) return color

    if (Array.isArray(color)) {
      const gradient = this.ctx.createLinearGradient(
        this.center[0],
        0,
        this.center[0],
        this.canvas.height
      )

      color.forEach(item => {
        gradient.addColorStop(item.offset, item.color)
      })

      return gradient
    }

    // 去掉首尾空格、多余空格、换行符
    color = color.trim().replace(/\n/g, "").replace(/\s{2,}/g, ' ')

    if (color.startsWith('linear-gradient')) return this.createLinearGradient(color)

    if (color.startsWith('radial-gradient')) return this.createRadialGradient(color)

    if (color.startsWith('conic-gradient')) return this.createConicGradient(color)

    return color
  }

  // 创建正弦曲线 (y = Asin(ωx + φ) + k)
  private createSineCurve(initialφ: number) {
    const a = Math.min(this.innerRadius / 10, 30) // 振幅
    const ω = ((2 * Math.PI) / (this.innerRadius * 2)) // 角速度
    const baseHeight = (this.canvas.height - 2 * this.innerRadius) / 2 // 基准高度
    const k = this.canvas.height - baseHeight - (2 * this.innerRadius * this.currentValue / 100) // 偏移

    initialφ = initialφ || 0 // 初相

    this.ctx.beginPath()

    this.ctx.fillStyle = this.waveColor as LiquidStyle

    let x = 0

    while (x < this.canvas.width) {
      const y = a * Math.sin((ω * x) + initialφ + this.φ) + k

      this.ctx.lineTo(x, y);

      x++
    }

    this.ctx.lineTo(this.canvas.width, this.canvas.height)
    this.ctx.lineTo(0, this.canvas.height)

    this.ctx.closePath()
    this.ctx.fill()
  }

  // 绘制未激活外圆
  private renderInActiveOuterCircle() {
    this.ctx.beginPath()

    this.ctx.strokeStyle = this.strokeInActiveColor as LiquidStyle
    this.ctx.lineWidth = this.pixelToDevice(this.strokeWidth)

    this.ctx.arc(this.center[0], this.center[1], this.outerRadius, 0, 2 * Math.PI)

    this.ctx.stroke()
  }

  // 绘制激活外圆
  private renderActiveOuterCircle() {
    if (!this.currentValue) return

    this.ctx.beginPath()

    this.ctx.lineCap = this.strokeLineCap
    this.ctx.strokeStyle = this.strokeActiveColor as LiquidStyle
    this.ctx.lineWidth = this.pixelToDevice(this.strokeWidth)

    const startAngle = -0.5 * Math.PI
    const endAngle = -Math.PI / 2 + (this.currentValue / 100) * 2 * Math.PI

    this.ctx.arc(this.center[0], this.center[1], this.outerRadius, startAngle, endAngle)

    this.ctx.stroke()
  }

  // 绘制间隔圆
  private renderInActiveInnerCircle() {
    if (!this.gap) return

    this.ctx.beginPath()

    this.ctx.strokeStyle = this.gapColor as LiquidStyle
    this.ctx.lineWidth = this.pixelToDevice(this.gap)

    this.ctx.arc(this.center[0], this.center[1], this.innerRadius, 0, 2 * Math.PI)

    this.ctx.stroke()
  }

  // 绘制水波
  private renderWave() {
    this.ctx.globalAlpha = 0.5

    let i = 0

    while (i < this.waveCount) {
      this.createSineCurve(i * 0.5 * Math.PI)

      i++
    }

    this.ctx.globalAlpha = 1

    // 恢复画布状态
    this.ctx.restore()
  }

  // 绘制文本
  private renderText() {
    this.offscreenCtx.textBaseline = 'middle'
    this.offscreenCtx.font = `bold ${this.integerFontSize}px Microsoft Yahei`

    this.offscreenCtx.fillText(this.currentInteger, 0, 0)

    const { width: integerWidth } = this.offscreenCtx.measureText(this.currentInteger)

    this.offscreenCtx.font = `bold ${this.decimalFontSize}px Microsoft Yahei`

    let decimalWidth = 0

    if (this.currentDecimal) {
      this.offscreenCtx.fillText(this.currentDecimal, integerWidth, 0)

      decimalWidth = this.offscreenCtx.measureText(this.currentDecimal).width
    }

    this.offscreenCtx.font = `bold ${this.decimalFontSize}px Microsoft Yahei`

    this.offscreenCtx.fillText('%', integerWidth + decimalWidth, 0)

    const { width: unitWidth } = this.offscreenCtx.measureText('%')

    this.ctx.globalCompositeOperation = 'difference'
    this.ctx.fillStyle = this.color as LiquidStyle
    this.ctx.textBaseline = 'middle'
    this.ctx.font = `bold ${this.integerFontSize}px Microsoft Yahei`

    const integerX = Math.floor((this.canvas.width - (integerWidth + decimalWidth + unitWidth)) / 2)
    const integerY = Math.floor((this.canvas.height) / 2)

    this.ctx.fillText(this.currentInteger, integerX, integerY)

    this.ctx.font = `bold ${this.decimalFontSize}px Microsoft Yahei`

    if (this.currentDecimal) {
      const decimalX = Math.floor(integerX + integerWidth)
      const decimalY = Math.floor((this.canvas.height + this.decimalFontSize / 2) / 2)

      this.ctx.fillText(this.currentDecimal, decimalX, decimalY)
    }

    const unitX = Math.floor(integerX + integerWidth + decimalWidth)
    const unitY = Math.floor((this.canvas.height + this.decimalFontSize / 2) / 2)

    this.ctx.fillText('%', unitX, unitY)

    this.ctx.globalCompositeOperation = 'source-over'
  }

  // 执行动画
  private animate() {
    if (this.raf) cancelAnimationFrame(this.raf)

    const currentValue = this.currentValue + this.step

    this.currentValue = this.isDown ? Math.max(currentValue, this.targetValue) : Math.min(currentValue, this.targetValue)

    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)

    // 保存画布状态
    this.ctx.save()

    // 创建裁剪区域
    this.ctx.beginPath()
    this.ctx.arc(this.center[0], this.center[1], this.outerRadius, 0, 2 * Math.PI)
    this.ctx.clip()

    this.renderWave()

    this.renderInActiveOuterCircle()

    this.renderActiveOuterCircle()

    this.renderInActiveInnerCircle()

    this.renderText()

    this.φ += 0.02

    this.raf = requestAnimationFrame(this.animate.bind(this))
  }

  // 绘制
  public draw(value: number) {
    value = Math.min(Math.max(value, this.minValue), this.maxValue)

    if (this.targetValue === value) return

    this.targetValue = value

    this.animate()
  }

  // 设置 canvas 尺寸
  public resize() {
    const { width, height } = this.canvas.getBoundingClientRect()

    this.canvas.width = this.pixelToDevice(width)
    this.canvas.height = this.pixelToDevice(height)

    this.ctx = this.canvas.getContext('2d')

    this.offscreenCanvas.width = this.pixelToDevice(width)
    this.offscreenCanvas.height = this.pixelToDevice(height)

    this.offscreenCtx = this.offscreenCanvas.getContext('2d')
    this.offscreenCtx.translate(0, this.offscreenCanvas.height)

    this.color = this.createLiquidColor(this.options.color)
    this.waveColor = this.createLiquidColor(this.options.waveColor || this.options.color)
    this.gapColor = this.createLiquidColor(this.options.gapColor)
    this.strokeInActiveColor = this.createLiquidColor(this.options.strokeInActiveColor)
    this.strokeActiveColor = this.createLiquidColor(this.options.strokeActiveColor || this.options.color)

    this.animate()
  }

  // 销毁 resize observer
  public disconnect() {
    this.resizeObserver.disconnect()

    this.resizeObserver = null
  }
}